package org.oregan.xpi;

import java.io.*;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Properties;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.zip.CRC32;
import java.util.zip.ZipOutputStream;
import java.util.zip.ZipEntry;

public abstract class XPISigner
{
    public static final String DIGEST_MANIFST_MESSAGE =
        "Name: {0}\n" +
            "Digest-Algorithms: MD5 SHA1\n" +
            "MD5-Digest: {1}\n" +
            "SHA1-Digest: {2}\n";

    public static final String DIGEST_MESSAGE =
        "Digest-Algorithms: MD5 SHA1\n" +
            "MD5-Digest: {0}\n" +
            "SHA1-Digest: {1}\n";

    static final String PERSONALISATION = "Created-by: XPISigner\n" +
        "XPI-Signer-Version: 2.0 http://o-regan.org\n";

    static final String MANIFEST_PREAMBLE =
        "Manifest-Version: 1.0\n" +
            PERSONALISATION +
            "Comments: PLEASE DO NOT EDIT THIS FILE. YOU WILL BREAK IT.\n";

    static final String ZIGBERT_PREAMBLE =
        "Signature-Version: 1.0\n" +
            PERSONALISATION +
            "Comments: PLEASE DO NOT EDIT THIS FILE. YOU WILL BREAK IT.\n";


    public static String ZIGBERT = "zigbert";

    public static String SIGNATURE_FILE_NAME = "META-INF/" + ZIGBERT + ".rsa";
    public static String SIGNATURE_MANIFEST_FILE_NAME = "META-INF/" + ZIGBERT + ".sf";
    public static String MANIFEST_FILE_NAME = "META-INF/manifest.mf";


    private MessageDigest digestSHA = null;
    private MessageDigest digestMD5 = null;

    private StringBuffer manifest;
    private StringBuffer zigbert;
    private String outputFile;
    private String pfxfile;
    private String password;

    private ArrayList listing;

    protected String signerDN = "";
    protected Boolean verbose;
    private File baseDir;
    private ArrayList<FileInfo> jarOrder;
    private ProgressObserver observer;
    protected Hashtable properties;

    public XPISigner(String pfxfile, String password, String baseDir, ArrayList listing, String outputFile)
    {
        try
        {
            this.pfxfile = pfxfile;
            this.password = password;
            this.baseDir = new File(baseDir);
            this.listing = listing;
            this.outputFile = outputFile;
            this.verbose = Boolean.valueOf(System.getProperty("xpi.verbose", "false"));

            try
            {
                digestSHA = MessageDigest.getInstance("SHA1");
                digestMD5 = MessageDigest.getInstance("MD5");
            } catch (NoSuchAlgorithmException e)
            {
                digestMD5 = null;
                digestSHA = null;
                e.printStackTrace();
            }

            // Null observer
            observer = new ProgressObserver()
            {
                public void setRange(int min, int max)
                {
                }

                public void setValue(int value)
                {
                }

                public void printMessage(String message)
                {
                }
            };
            properties = new Properties();
        } catch (Exception e)
        {
            e.printStackTrace();
        }
    }

    public void setProperty(String key, Object value)
    {
        properties.put(key, value);
    }

    public void setProgressObserver(ProgressObserver external)
    {
        observer = external;
    }

    protected void println(String s)
    {
        if (verbose)
            Main.logMessage(s + "\n");

        observer.printMessage(s);
    }

    protected void print(String s)
    {
        if (verbose)
            System.out.print(s);

        observer.printMessage(s);
    }

    public XPIInfo generateXPI() throws XPIException
    {
        try
        {

            compileManifests();
            return generateArchive();

        } catch (IOException e)
        {
            //
        }
        return null;
    }

    public XPIInfo generateArchive()
        throws IOException, XPIException
    {
        File outFile = new File(outputFile);
        if (outFile.exists())
        {
            System.err.println("Overwriting: " + outFile);
        } else
        {
            outFile.createNewFile();
        }

        jarOrder.add(0, inspectFile(SIGNATURE_FILE_NAME, false)); // rsa file at start and uncompressed.
        jarOrder.add(inspectFile(MANIFEST_FILE_NAME));
        jarOrder.add(inspectFile(SIGNATURE_MANIFEST_FILE_NAME));


        XPIInfo info = new XPIInfo(outFile, signerDN, jarOrder.size());

        print("Saving XPI ...");


        boolean useZip = false;

        if (!System.getProperty("xpi.zip", "false").equalsIgnoreCase("false"))
        {
            useZip = true;
        }

        if (useZip)
        {

            if(outFile.exists())
            {
                if(outFile.delete())
                {

                }

            }



            final StringBuffer sb = new StringBuffer();


            for (FileInfo fi : jarOrder)
            {
                sb.append(fi.getName()).append('\n');
            }

            //System.out.println(sb);

            File filelisting = File.createTempFile("xpi.", ".lst");

            filelisting.deleteOnExit();

            Utils.saveMessage(sb.toString().getBytes(), filelisting);


            String outfilepath = outFile.getAbsolutePath();

            File script = new File(System.getProperty("xpi.home"), "dozip.sh");

            if (!script.exists())
            {
                System.out.println("xpi.home was not passed to java runtime. Cannot find dozip.sh");
                throw new XPIException("setup incomplete", Main.ERR_FILE_NOT_FOUND);
            }


            ProcessBuilder pb = new ProcessBuilder("sh", script.getAbsolutePath(), outfilepath, filelisting.getAbsolutePath());
            pb.directory(baseDir);
            pb.redirectErrorStream(true);

            final Process p = pb.start();


            try
            {
                int i = p.waitFor();
                System.out.println("Result from zip was: " + i);

            } catch (InterruptedException e)
            {
                e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
            }


        } else
        {

            JarOutputStream jos = new JarOutputStream(new FileOutputStream(outFile));

            observer.setRange(0, jarOrder.size());
            observer.setValue(0);
            int i = 0;

            for (FileInfo fi : jarOrder)
            {
                jarFile(jos, fi);
                observer.setValue(++i);
            }

            jos.close();
        }
        return info;
    }

    public void compileManifests()
        throws XPIException, IOException
    {
        if (digestMD5 == null || digestSHA == null)
            throw new XPIException("Failed to initialise hash algorithms", Main.ERR_NO_DIGEST_ALGS);

        if(properties.containsKey("signature.alias"))
        {
            ZIGBERT = (String) properties.get("signature.alias");
        }

        SIGNATURE_FILE_NAME = "META-INF/" + ZIGBERT + ".rsa";
        SIGNATURE_MANIFEST_FILE_NAME = "META-INF/" + ZIGBERT + ".sf";

        manifest = new StringBuffer();
        zigbert = new StringBuffer();

        jarOrder = new ArrayList<FileInfo>();

        println("Starting scan of files...");

        startManifestEntries();
        observer.setRange(0, listing.size());

        int i = 0;
        for (Object o : listing)
        {
            String line = (String) o;
            try
            {
                FileInfo metadata = inspectFile(line);
                jarOrder.add(metadata);
                addManifestEntry(metadata);

                observer.setValue(++i);

                println(line);
                println("\tMD5   " + Utils.toHexString(metadata.getMd5()).toUpperCase());
                println("\tSHA-1 " + Utils.toHexString(metadata.getSha1()).toUpperCase());
                println("");

            } catch (XPIException e)
            {
                System.err.println(e.getMessage());
                System.exit(e.getErr());
            }
        }

        File f = new File(baseDir, MANIFEST_FILE_NAME);
        if (!f.getParentFile().exists())
            f.getParentFile().mkdirs();

        print("Saving manifest file...");
        Utils.saveMessage((manifest.toString().trim() + "\n").getBytes("LATIN1"), f);
        println("done.");
        print("Saving signature file...");

        byte[] zbytes = (zigbert.toString().trim() + "\n").getBytes("LATIN1");
        f = new File(baseDir, SIGNATURE_MANIFEST_FILE_NAME);

        Utils.saveMessage(zbytes, f);
        println("done.");

        print("Loading credential and signing ...");

        File location = checkLocation(pfxfile, password);

        if (location == null)
        {
            System.err.println("Failed to find credential at " + pfxfile);
            System.exit(Main.ERR_FILE_NOT_FOUND);
        }
        byte[] signature = sign(zbytes, location, password);

        f = new File(baseDir, SIGNATURE_FILE_NAME);

        Utils.saveMessage(signature, f);
        println("done.");
    }


    FileInfo inspectFile(String filePath) throws XPIException
    {
        return inspectFile(filePath, true);
    }

    /**
     * Inspect the file and build a fileinfo object
     *
     * @param filePath
     * @param compress
     * @return
     */
    private FileInfo inspectFile(String filePath, boolean compress) throws XPIException
    {
        long length = 0;
        long crc = 0;
        byte[] md5 = new byte[0];
        byte[] sha1 = new byte[0];

        File tmp = new File(baseDir, filePath);
        try {
            tmp = tmp.getCanonicalFile();
        } catch (IOException e) {
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        }
        if (!tmp.exists())
            throw new XPIException("  File \'" + tmp.getAbsolutePath() + "\' does not exist.", Main.ERR_FILE_NOT_FOUND);

        try
        {

            length = tmp.length();

            BufferedInputStream bis = null;
            try
            {
                bis = new BufferedInputStream(new FileInputStream(tmp));
            } catch (FileNotFoundException e1)
            {
                // handled above
            }

            digestMD5.reset();
            digestSHA.reset();

            DigestInputStream md5Stream = new DigestInputStream(bis, digestMD5);
            DigestInputStream shaStream = new DigestInputStream(md5Stream, digestSHA);

            CRC32 crc32 = new CRC32();

            byte[] data = new byte[1024 * 2];
            int byteCount;
            while ((byteCount = shaStream.read(data)) > -1)
            {
                crc32.update(data, 0, byteCount);
            }
            crc = crc32.getValue();
            crc32.reset();
            md5 = md5Stream.getMessageDigest().digest();
            sha1 = shaStream.getMessageDigest().digest();
            bis.close();
        } catch (IOException e1)
        {
            throw new XPIException("Error reading from \'" + tmp + "\'", Main.ERR_ERROR_READING_FILE);
        }


        return new FileInfo(filePath, length, crc, md5, sha1, compress);

    }

    void startManifestEntries()
    {
        digestMD5.reset();
        digestSHA.reset();

        byte[] bytes = null;
        try
        {
            bytes = MANIFEST_PREAMBLE.getBytes("LATIN1");
        } catch (UnsupportedEncodingException e)
        {
            //
        }
        byte[] md5 = digestMD5.digest(bytes);
        byte[] sha = digestSHA.digest(bytes);

        String md5Text = Utils.toB64(md5);
        String shaText = Utils.toB64(sha);

        String zigbertR = MessageFormat.format(DIGEST_MESSAGE, md5Text, shaText);

        manifest.append(MANIFEST_PREAMBLE).append("\n");
        zigbert.append(ZIGBERT_PREAMBLE).append(zigbertR).append("\n");
    }

    void addManifestEntry(FileInfo fileInfo)
    {

        String name = fileInfo.getName();


        String md5Text = Utils.toB64(fileInfo.getMd5());
        String sha1Text = Utils.toB64(fileInfo.getSha1());


        String mfest = MessageFormat.format(DIGEST_MANIFST_MESSAGE, name, md5Text, sha1Text);

        manifest.append(mfest).append("\n");

        digestMD5.reset();
        digestSHA.reset();

        try
        {
            byte[] bytes = mfest.getBytes("LATIN1");
            md5Text = Utils.toB64(digestMD5.digest(bytes));
            sha1Text = Utils.toB64(digestSHA.digest(bytes));

            String ziggy = MessageFormat.format(DIGEST_MANIFST_MESSAGE, name, md5Text, sha1Text);
            zigbert.append(ziggy).append("\n");

        } catch (UnsupportedEncodingException e)
        {
            e.printStackTrace();
        }
    }

    void jarFile(JarOutputStream stream, FileInfo metadata)
    {
        try
        {
            String filePath = metadata.getName();

            File f = new File(baseDir, filePath);

            BufferedInputStream bis = new BufferedInputStream(new FileInputStream(f));

            JarEntry fileEntry = new JarEntry(filePath);

            if (metadata.isCompressed())
            {
                fileEntry.setMethod(JarEntry.DEFLATED);
            } else
            {
                fileEntry.setMethod(JarEntry.STORED);
                fileEntry.setSize(metadata.getLength());
                fileEntry.setCompressedSize(metadata.getLength());
                fileEntry.setCrc(metadata.getCRC());
            }
            stream.putNextEntry(fileEntry);
            Utils.slurp(bis, stream);
            stream.closeEntry();
            bis.close();

        } catch (IOException e)
        {
            e.printStackTrace();
        }
    }


    public abstract byte[] sign(byte[] zbytes, File location, String password) throws XPIException;

    public abstract File checkLocation(String location, String password);


}
